package com.example.controllers;

import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;

import java.io.IOException;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.http.MockHttpOutputMessage;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;

import com.example.ShoppingCartApiApplication;
import com.example.dtos.ItemToPurchaseDTO;
import com.example.dtos.ProductDTO;
import com.example.models.Cart;
import com.example.models.Customer;
import com.example.models.ItemToPurchase;
import com.example.models.Product;
import com.example.repositories.ProductRepository;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = ShoppingCartApiApplication.class)
@WebAppConfiguration
@IntegrationTest({"server.port=9090"})
public class ProductControllerIT {
	
	private MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
            MediaType.APPLICATION_JSON.getSubtype(),
            Charset.forName("utf8"));

    private MockMvc mockMvc;
    
    private HttpMessageConverter mappingJackson2HttpMessageConverter;
    
    private Product product1, product2, product3, product4;
    SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy");
    DateFormat df = new SimpleDateFormat("dd-MM-yyyy");
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private WebApplicationContext webApplicationContext;

    @Autowired
    void setConverters(HttpMessageConverter<?>[] converters) {

        this.mappingJackson2HttpMessageConverter = Arrays.asList(converters).stream().filter(
                hmc -> hmc instanceof MappingJackson2HttpMessageConverter).findAny().get();

        Assert.assertNotNull("the JSON message converter must not be null",
                this.mappingJackson2HttpMessageConverter);
    }
    
    @Before
    public void setup() throws Exception {
        this.mockMvc = webAppContextSetup(webApplicationContext).build();

        this.productRepository.deleteAll();
        
        product1 = productRepository.save(new Product("p1Id","food","pizza",50.55,200, formatter.parse("10-01-2023")));
        product2 = productRepository.save(new Product("p2Id","drink","beer",26.90,100, formatter.parse("10-01-2021")));
        product3 = productRepository.save(new Product("p3Id","drink","coke",26.90,400, formatter.parse("10-01-2022")));;
        product4 = productRepository.save(new Product("p4Id","fruit","apple",26.90,300, formatter.parse("10-01-2019")));
    }
    
    @Test
	public void productNotFound() throws Exception{
		 mockMvc.perform(get("/products/123")
		 			.contentType(contentType))
	                .andExpect(status().isNotFound());
	}
    
    @Test
    public void readSingleProduct() throws Exception {
        mockMvc.perform(get("/products/"
                + this.product1.getId()))
                .andExpect(status().isOk())
                .andExpect(content().contentType(contentType))
                .andExpect(jsonPath("$.data", is(this.product1.getId()+"/"+this.product1.getName()+
                		"/"+this.product1.getCategory()+"/"+this.product1.getPrice()+"/"+this.product1.getStock()
                		+"/"+this.product1.getExpirationDate())));
//                .andExpect(jsonPath("$.id", is(this.product1.getId())))
//                .andExpect(jsonPath("$.category", is(this.product1.getCategory())))
//                .andExpect(jsonPath("$.name", is(this.product1.getName())))
//                .andExpect(jsonPath("$.price", is(this.product1.getPrice())))
//                .andExpect(jsonPath("$.stock", is(this.product1.getStock())));
    }
	
	@Test
	public void deleteProduct() throws Exception {
		mockMvc.perform(delete("/products/"
                + this.product1.getId()))
				.andExpect(status().isNoContent());
	}
	
	@Test
	public void deleteProductWhenNotFound() throws Exception {
		mockMvc.perform(delete("/products/"
                + "123"))
				.andExpect(status().isNotFound());
	}
	
	@Test
	public void createProduct() throws Exception {
	        String productJson = json(new ProductDTO("pNewId","newCat/newName/10/100/10-10-2020"));
	        this.mockMvc.perform(post("/products")
	                .contentType(contentType)
	                .content(productJson))
	                .andExpect(status().isCreated());
	}
	
	@Test
    public void readAllProducts() throws Exception {
        mockMvc.perform(get("/products"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(contentType))
                .andExpect(jsonPath("$.content", hasSize(4)))
                .andExpect(jsonPath("$.content[0].data", is(
                  	  this.product1.getId()+"/"+this.product1.getName()+"/"+this.product1.getCategory()+"/"+this.product1.getPrice()+
                  	  "/"+this.product1.getStock()+"/"+this.product1.getExpirationDate())))
  				.andExpect(jsonPath("$.content[1].data", is(
  		          	  this.product2.getId()+"/"+this.product2.getName()+"/"+this.product2.getCategory()+"/"+this.product2.getPrice()+
  		          	  "/"+this.product2.getStock()+"/"+this.product2.getExpirationDate())))
  				.andExpect(jsonPath("$.content[2].data", is(
    		          	  this.product3.getId()+"/"+this.product3.getName()+"/"+this.product3.getCategory()+"/"+this.product3.getPrice()+
    		          	  "/"+this.product3.getStock()+"/"+this.product3.getExpirationDate())))
  				.andExpect(jsonPath("$.content[3].data", is(
    		          	  this.product4.getId()+"/"+this.product4.getName()+"/"+this.product4.getCategory()+"/"+this.product4.getPrice()+
    		          	  "/"+this.product4.getStock()+"/"+this.product4.getExpirationDate())));
    }
	
	@Test
	public void readAllSortedBy() throws Exception {
		mockMvc.perform(get("/products?sort=stock"))
		.andExpect(content().contentType(contentType))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content", hasSize(4)))
        .andExpect(jsonPath("$.content[0].data", is(this.product2.getId()+"/"+this.product2.getName()+"/"+this.product2.getCategory()+"/"+this.product2.getPrice()+
        		"/"+this.product2.getStock()+"/"+this.product2.getExpirationDate())))
        .andExpect(jsonPath("$.content[1].data", is(this.product1.getId()+"/"+this.product1.getName()+"/"+this.product1.getCategory()+"/"+this.product1.getPrice()+
        		"/"+this.product1.getStock()+"/"+this.product1.getExpirationDate())))
        .andExpect(jsonPath("$.content[2].data", is(this.product4.getId()+"/"+this.product4.getName()+"/"+this.product4.getCategory()+"/"+this.product4.getPrice()+
        		"/"+this.product4.getStock()+"/"+this.product4.getExpirationDate())))
        .andExpect(jsonPath("$.content[3].data", is(this.product3.getId()+"/"+this.product3.getName()+"/"+this.product3.getCategory()+"/"+this.product3.getPrice()+
        		"/"+this.product3.getStock()+"/"+this.product3.getExpirationDate())));	
	}
	
	@Test
	public void readAllSortedByMultipleOrders() throws Exception {
		productRepository.delete(product1);
		productRepository.delete(product4);
		
		mockMvc.perform(get("/products?sort=category|name"))
		.andExpect(content().contentType(contentType))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content", hasSize(2)))
        .andExpect(jsonPath("$.content[0].data", is(this.product2.getId()+"/"+this.product2.getName()+"/"+this.product2.getCategory()+"/"+this.product2.getPrice()+"/"+this.product2.getStock()+"/"+
	    		  this.product2.getExpirationDate())))
      .andExpect(jsonPath("$.content[1].data", is(this.product3.getId()+"/"+this.product3.getName()+"/"+this.product3.getCategory()+"/"+this.product3.getPrice()+"/"+this.product3.getStock()+"/"+
	    		  this.product3.getExpirationDate())));
		
		mockMvc.perform(get("/products?sort=category|-name"))
		.andExpect(content().contentType(contentType))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content", hasSize(2)))
        .andExpect(jsonPath("$.content[0].data", is(this.product3.getId()+"/"+this.product3.getName()+"/"+this.product3.getCategory()+"/"+this.product3.getPrice()+"/"+this.product3.getStock()+"/"+
	    		  this.product3.getExpirationDate())))
        .andExpect(jsonPath("$.content[1].data", is(this.product2.getId()+"/"+this.product2.getName()+"/"+this.product2.getCategory()+"/"+this.product2.getPrice()+"/"+this.product2.getStock()+"/"+
	    		  this.product2.getExpirationDate())));
	}
	
	@Test
	public void readAllPaginated() throws Exception {
		mockMvc.perform(get("/products?page=0&size=2"))
		.andExpect(content().contentType(contentType))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content", hasSize(2)))
        .andExpect(jsonPath("$.content[0].data", is(this.product1.getId()+"/"+this.product1.getName()+"/"+this.product1.getCategory()+"/"+this.product1.getPrice()+
        		"/"+this.product1.getStock()+"/"+this.product1.getExpirationDate())))
        .andExpect(jsonPath("$.content[1].data", is(this.product2.getId()+"/"+this.product2.getName()+"/"+this.product2.getCategory()+"/"+this.product2.getPrice()+
        		"/"+this.product2.getStock()+"/"+this.product2.getExpirationDate())));
		mockMvc.perform(get("/products?page=1&size=2"))
		.andExpect(content().contentType(contentType))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content", hasSize(2)))
        .andExpect(jsonPath("$.content[0].data", is(this.product3.getId()+"/"+this.product3.getName()+"/"+this.product3.getCategory()+"/"+this.product3.getPrice()+
        		"/"+this.product3.getStock()+"/"+this.product3.getExpirationDate())))
		.andExpect(jsonPath("$.content[1].data", is(this.product4.getId()+"/"+this.product4.getName()+"/"+this.product4.getCategory()+"/"+this.product4.getPrice()+
				"/"+this.product4.getStock()+"/"+this.product4.getExpirationDate())));
	}
	
	@Test
	public void readAllPaginatedAndSorted() throws Exception {
		//p4:fruit p3:food p2 and p1: drink
		mockMvc.perform(get("/products?page=0&size=2&sort=-category"))
		.andExpect(content().contentType(contentType))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content", hasSize(2)))
		.andExpect(jsonPath("$.content[0].data", is(this.product4.getId()+"/"+this.product4.getName()+"/"+this.product4.getCategory()+"/"+this.product4.getPrice()+
				"/"+this.product4.getStock()+"/"+this.product4.getExpirationDate())))
        .andExpect(jsonPath("$.content[1].data", is(this.product1.getId()+"/"+this.product1.getName()+"/"+this.product1.getCategory()+"/"+this.product1.getPrice()+
        		"/"+this.product1.getStock()+"/"+this.product1.getExpirationDate())));
		mockMvc.perform(get("/products?page=1&size=2&sort=-category"))
		.andExpect(content().contentType(contentType))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content", hasSize(2)))
        .andExpect(jsonPath("$.content[0].data", is(this.product2.getId()+"/"+this.product2.getName()+"/"+this.product2.getCategory()+"/"+this.product2.getPrice()+
        		"/"+this.product2.getStock()+"/"+this.product2.getExpirationDate())))
        .andExpect(jsonPath("$.content[1].data", is(this.product3.getId()+"/"+this.product3.getName()+"/"+this.product3.getCategory()+"/"+this.product3.getPrice()+
        		"/"+this.product3.getStock()+"/"+this.product3.getExpirationDate())));
	}
	
	@Test
	public void updateProduct() throws Exception {
        product1.setName("new Name!");
        product1.setCategory("new Category!");
        ProductDTO productToUpdate =  new ProductDTO(product1.getId(), this.product1.getName()+"/"+
	    		  this.product1.getCategory()+"/"+
	    		  this.product1.getPrice()+"/"+
	    		  this.product1.getStock()+"/"+df.format(this.product1.getExpirationDate()));
        String productJson = json(productToUpdate);
		mockMvc.perform(put("/products/"+this.product1.getId())
                .contentType(contentType)
                .content(productJson))
                .andExpect(status().isOk());
	}
	
	 @Test
	 public void updateProductWhenNotFound() throws Exception{
		 	ProductDTO productToUpdate =  new ProductDTO(product1.getId(), this.product1.getName()+"/"+
		    		  this.product1.getCategory()+"/"+
		    		  this.product1.getPrice()+"/"+
		    		  this.product1.getStock()+"/"+df.format(this.product1.getExpirationDate()));
		 	String productJson = json(productToUpdate);
			 mockMvc.perform(put("/products/123")
			 			.contentType(contentType)
			 			.content(productJson))
		                .andExpect(status().isNotFound());
	 }
//	 
//	 @Test
//	 public void buyProduct() throws Exception {
//		 	String itemToPurchaseJson = json(new ItemToPurchaseDTO("itemId", 10, product1.getId()));
//	        mockMvc.perform(put("/products/buy")
//	        		.contentType(contentType)
//	        		.content(itemToPurchaseJson))
//	        		.andExpect(status().isOk());
//	        //shall i check the product stock has been decremented?
//	 }
//	 
	 @Test
	    public void readProductsWithCategory() throws Exception {
			 mockMvc.perform(get("/products/category/cuchuflo"))
	         .andExpect(status().isOk())
	         .andExpect(content().contentType(contentType))
	         .andExpect(jsonPath("$", hasSize(0)));
			mockMvc.perform(get("/products/category/"+product1.getCategory()))
			        .andExpect(status().isOk())
			        .andExpect(content().contentType(contentType))
			        .andExpect(jsonPath("$", hasSize(1)))
			        .andExpect(jsonPath("$[0].data", is(
			          	  this.product1.getId()+"/"+
				    		  this.product1.getName()+"/"+
				    		  this.product1.getCategory()+"/"+
				    		  this.product1.getPrice()+"/"+
				    		  this.product1.getStock()+"/"+this.product1.getExpirationDate())));
			mockMvc.perform(get("/products/category/"+product4.getCategory()))
			        .andExpect(status().isOk())
			        .andExpect(content().contentType(contentType))
			        .andExpect(jsonPath("$", hasSize(1)))
					.andExpect(jsonPath("$[0].data", is(
				          	  this.product4.getId()+"/"+
					    		  this.product4.getName()+"/"+
					    		  this.product4.getCategory()+"/"+
					    		  this.product4.getPrice()+"/"+
					    		  this.product4.getStock()+"/"+this.product4.getExpirationDate())));
	    }
	 
	 @Test
	    public void readProductsWithName() throws Exception {
			 mockMvc.perform(get("/products/name/cuchuflo"))
	         .andExpect(status().isOk())
	         .andExpect(content().contentType(contentType))
	         .andExpect(jsonPath("$", hasSize(0)));
			mockMvc.perform(get("/products/name/"+product1.getName()))
			        .andExpect(status().isOk())
			        .andExpect(content().contentType(contentType))
			        .andExpect(jsonPath("$", hasSize(1)))
			        .andExpect(jsonPath("$[0].data", is(
			              	  this.product1.getId()+"/"+
					    		  this.product1.getName()+"/"+
					    		  this.product1.getCategory()+"/"+
					    		  this.product1.getPrice()+"/"+
					    		  this.product1.getStock()+"/"+this.product1.getExpirationDate())));
			mockMvc.perform(get("/products/name/"+product2.getName()))
			    .andExpect(status().isOk())
			    .andExpect(content().contentType(contentType))
			    .andExpect(jsonPath("$", hasSize(1)))
			    .andExpect(jsonPath("$[0].data", is(
			          	  this.product2.getId()+"/"+
				    		  this.product2.getName()+"/"+
				    		  this.product2.getCategory()+"/"+
				    		  this.product2.getPrice()+"/"+
				    		  this.product2.getStock()+"/"+this.product2.getExpirationDate())));
	        
	  }
	 
	 @Test
	 public void readProductsWithDateBetween() throws Exception {
		 String date1="10-01-2018";
		 String date2="10-12-2021";
		 
	     mockMvc.perform(get("/products/dateBetween/"+date1+"/"+date2))
	            .andExpect(status().isOk())
	            .andExpect(content().contentType(contentType))
	            .andExpect(jsonPath("$", hasSize(2)))
	            .andExpect(jsonPath("$[0].data", is(
			          	  this.product2.getId()+"/"+
				    		  this.product2.getName()+"/"+
				    		  this.product2.getCategory()+"/"+
				    		  this.product2.getPrice()+"/"+
				    		  this.product2.getStock()+"/"+
	    		    		  this.product2.getExpirationDate())))
	            .andExpect(jsonPath("$[1].data", is(
			          	  this.product4.getId()+"/"+
				    		  this.product4.getName()+"/"+
				    		  this.product4.getCategory()+"/"+
				    		  this.product4.getPrice()+"/"+
				    		  this.product4.getStock()+"/"+
	    		    		  this.product4.getExpirationDate())));
	        
	    }
	

	protected String json(Object o) throws IOException {
	        MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage();
	        this.mappingJackson2HttpMessageConverter.write(
	                o, MediaType.APPLICATION_JSON, mockHttpOutputMessage);
	        return mockHttpOutputMessage.getBodyAsString();
	}
}
